<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Fixer\PhpUnit;

use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Fixer\PhpUnit\PhpUnitTargetVersion;
use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
use PhpCsFixer\Utils;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer>
 *
 * @author Filippo Tessarotto <zoeslam@gmail.com>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class PhpUnitTestCaseStaticMethodCallsFixerTest extends AbstractFixerTestCase
{
    public function testFixerContainsAllPhpunitStaticMethodsInItsList(): void
    {
        $assertionRefClass = new \ReflectionClass(TestCase::class);
        $updatedStaticMethodsList = $assertionRefClass->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED);

        $methods = (new \ReflectionClass(PhpUnitTestCaseStaticMethodCallsFixer::class))->getConstant('METHODS');

        $missingMethods = [];
        foreach ($updatedStaticMethodsList as $method) {
            if ($method->isStatic() && !isset($methods[$method->name])) {
                $missingMethods[] = $method->name;
            }
        }

        self::assertSame([], $missingMethods, \sprintf(
            'The following static methods from "%s" are missing from "%s::METHODS": %s',
            TestCase::class,
            PhpUnitTestCaseStaticMethodCallsFixer::class,
            // `Utils::naturalLanguageJoin` does not accept empty array, so let's use it only if there's an actual difference.
            [] === $missingMethods ? '' : Utils::naturalLanguageJoin($missingMethods)
        ));
    }

    /**
     * @param array<string, mixed> $configuration
     *
     * @dataProvider provideInvalidConfigurationCases
     */
    public function testInvalidConfiguration(array $configuration, string $expectedExceptionMessage): void
    {
        $this->expectException(InvalidFixerConfigurationException::class);
        $this->expectExceptionMessage($expectedExceptionMessage);

        $this->fixer->configure($configuration);
    }

    /**
     * @return iterable<string, array{array<string, mixed>, string}>
     */
    public static function provideInvalidConfigurationCases(): iterable
    {
        yield 'wrong type for methods key' => [
            ['methods' => [123 => 1]],
            '[php_unit_test_case_static_method_calls] Invalid configuration: The option "methods" with value array is expected to be of type "string[]", but one of the elements is of type "int".',
        ];

        yield 'wrong type for methods value' => [
            ['methods' => ['assertSame' => 123]],
            '[php_unit_test_case_static_method_calls] Invalid configuration: The option "methods" with value array is expected to be of type "string[]", but one of the elements is of type "int".',
        ];
    }

    public function testWrongConfigTypeForMethodsAndTargetVersion(): void
    {
        $this->expectException(InvalidFixerConfigurationException::class);
        $this->expectExceptionMessage('[php_unit_test_case_static_method_calls] Configuration cannot contain method "once" and target "11.0", it is dynamic in that PHPUnit version.');

        $this->fixer->configure([
            'methods' => ['once' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_SELF],
            'target' => PhpUnitTargetVersion::VERSION_11_0,
        ]);
    }

    /**
     * @param _AutogeneratedInputConfiguration $config
     *
     * @dataProvider provideFixCases
     */
    public function testFix(string $expected, ?string $input = null, array $config = []): void
    {
        $this->fixer->configure($config);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<array{0: string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFixCases(): iterable
    {
        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        static::assertSame(1, 2);
                        static::markTestIncomplete('foo');
                        static::fail('foo');
                    }
                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        $this->assertSame(1, 2);
                        $this->markTestIncomplete('foo');
                        $this->fail('foo');
                    }
                }
                EOF,
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testMocks()
                    {
                        $mock = $this->createMock(MyInterface::class);
                        $mock
                            ->expects(static::once())
                            ->method('run')
                            ->with(
                                static::identicalTo(1),
                                static::stringContains('foo')
                            )
                            ->will(static::onConsecutiveCalls(
                                static::returnSelf(),
                                static::throwException(new \Exception())
                            ))
                        ;
                    }
                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testMocks()
                    {
                        $mock = $this->createMock(MyInterface::class);
                        $mock
                            ->expects($this->once())
                            ->method('run')
                            ->with(
                                $this->identicalTo(1),
                                $this->stringContains('foo')
                            )
                            ->will($this->onConsecutiveCalls(
                                $this->returnSelf(),
                                $this->throwException(new \Exception())
                            ))
                        ;
                    }
                }
                EOF,
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testWeirdIndentation()
                    {
                        static
                        // @TODO
                            ::
                        assertSame
                        (1, 2);
                        // $this->markTestIncomplete('foo');
                        /*
                        $this->fail('foo');
                        */
                    }
                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testWeirdIndentation()
                    {
                        $this
                        // @TODO
                            ->
                        assertSame
                        (1, 2);
                        // $this->markTestIncomplete('foo');
                        /*
                        $this->fail('foo');
                        */
                    }
                }
                EOF,
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        $this->assertSame(1, 2);
                        $this->markTestIncomplete('foo');
                        $this->fail('foo');

                        $lambda = function () {
                            $this->assertSame(1, 23);
                            self::assertSame(1, 23);
                            static::assertSame(1, 23);
                        };
                    }
                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        $this->assertSame(1, 2);
                        self::markTestIncomplete('foo');
                        static::fail('foo');

                        $lambda = function () {
                            $this->assertSame(1, 23);
                            self::assertSame(1, 23);
                            static::assertSame(1, 23);
                        };
                    }
                }
                EOF,
            ['call_type' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_THIS],
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        self::assertSame(1, 2);
                        self::markTestIncomplete('foo');
                        self::fail('foo');
                    }
                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        $this->assertSame(1, 2);
                        self::markTestIncomplete('foo');
                        static::fail('foo');
                    }
                }
                EOF,
            ['call_type' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_SELF],
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        $this->assertSame(1, 2);
                        $this->assertSame(1, 2);

                        static::setUpBeforeClass();
                        static::setUpBeforeClass();

                        $otherTest->setUpBeforeClass();
                        OtherTest::setUpBeforeClass();
                    }
                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function testBaseCase()
                    {
                        static::assertSame(1, 2);
                        $this->assertSame(1, 2);

                        static::setUpBeforeClass();
                        $this->setUpBeforeClass();

                        $otherTest->setUpBeforeClass();
                        OtherTest::setUpBeforeClass();
                    }
                }
                EOF,
            [
                'call_type' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_THIS,
                'methods' => ['setUpBeforeClass' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_STATIC],
            ],
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public static function foo()
                    {
                        $this->assertSame(1, 2);
                        self::assertSame(1, 2);
                        static::assertSame(1, 2);

                        $lambda = function () {
                            $this->assertSame(1, 2);
                            self::assertSame(1, 2);
                            static::assertSame(1, 2);
                        };
                    }

                    public function bar()
                    {
                        $lambda = static function () {
                            $this->assertSame(1, 2);
                            self::assertSame(1, 2);
                            static::assertSame(1, 2);
                        };

                        $myProphecy->setCount(0)->will(function () {
                            $this->getCount()->willReturn(0);
                        });
                    }

                    static public function baz()
                    {
                        $this->assertSame(1, 2);
                        self::assertSame(1, 2);
                        static::assertSame(1, 2);

                        $lambda = function () {
                            $this->assertSame(1, 2);
                            self::assertSame(1, 2);
                            static::assertSame(1, 2);
                        };
                    }

                    static final protected function xyz()
                    {
                        static::assertSame(1, 2);
                    }
                }
                EOF,
            null,
            [
                'call_type' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_THIS,
            ],
        ];

        yield [
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function foo()
                    {
                        $this->assertSame(1, 2);
                        $this->assertSame(1, 2);
                        $this->assertSame(1, 2);
                    }

                    public function bar()
                    {
                        $lambdaOne = static function () {
                            $this->assertSame(1, 21);
                            self::assertSame(1, 21);
                            static::assertSame(1, 21);
                        };

                        $lambdaTwo = function () {
                            $this->assertSame(1, 21);
                            self::assertSame(1, 21);
                            static::assertSame(1, 21);
                        };
                    }

                    public function baz2()
                    {
                        $this->assertSame(1, 22);
                        $this->assertSame(1, 22);
                        $this->assertSame(1, 22);
                        $this->assertSame(1, 23);
                    }

                }
                EOF,
            <<<'EOF'
                <?php
                class MyTest extends \PHPUnit_Framework_TestCase
                {
                    public function foo()
                    {
                        $this->assertSame(1, 2);
                        self::assertSame(1, 2);
                        static::assertSame(1, 2);
                    }

                    public function bar()
                    {
                        $lambdaOne = static function () {
                            $this->assertSame(1, 21);
                            self::assertSame(1, 21);
                            static::assertSame(1, 21);
                        };

                        $lambdaTwo = function () {
                            $this->assertSame(1, 21);
                            self::assertSame(1, 21);
                            static::assertSame(1, 21);
                        };
                    }

                    public function baz2()
                    {
                        $this->assertSame(1, 22);
                        self::assertSame(1, 22);
                        static::assertSame(1, 22);
                        STATIC::assertSame(1, 23);
                    }

                }
                EOF,
            [
                'call_type' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_THIS,
            ],
        ];

        yield 'do not change class property and method signature' => [
            <<<'EOF'
                <?php
                class FooTest extends TestCase
                {
                    public function foo()
                    {
                        $this->assertSame = 42;
                    }

                    public function assertSame($foo, $bar){}
                }
                EOF,
        ];

        yield 'do not change when only case is different' => [
            <<<'EOF'
                <?php
                class FooTest extends TestCase
                {
                    public function foo()
                    {
                        STATIC::assertSame(1, 1);
                    }
                }
                EOF,
        ];

        yield 'do not crash on abstract static function' => [
            <<<'EOF'
                <?php
                abstract class FooTest extends TestCase
                {
                    abstract public static function dataProvider();
                }
                EOF,
            null,
            [
                'call_type' => PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_THIS,
            ],
        ];

        yield 'handle $this with double colon following' => [
            '<?php
                class FooTest extends TestCase
                {
                    public function testFoo()
                    {
                        static::assertTrue(true);
                    }
                }',
            '<?php
                class FooTest extends TestCase
                {
                    public function testFoo()
                    {
                        $this::assertTrue(true);
                    }
                }',
        ];

        yield 'anonymous class' => [
            '<?php
class MyTest extends \PHPUnit_Framework_TestCase
{
    public function testBaseCase()
    {
        static::assertSame(1, 2);

        $foo = new class() {
            public function assertSame($a, $b)
            {
                $this->assertSame(1, 2);
            }
        };
    }
}',
            '<?php
class MyTest extends \PHPUnit_Framework_TestCase
{
    public function testBaseCase()
    {
        $this->assertSame(1, 2);

        $foo = new class() {
            public function assertSame($a, $b)
            {
                $this->assertSame(1, 2);
            }
        };
    }
}',
        ];

        yield 'anonymous class extending TestCase' => [
            <<<'PHP'
                <?php
                $myTest = new class () extends \PHPUnit_Framework_TestCase
                {
                    public function testSomething()
                    {
                        static::assertTrue(true);
                        static::assertTrue(true);
                    }
                };
                PHP,
            <<<'PHP'
                <?php
                $myTest = new class () extends \PHPUnit_Framework_TestCase
                {
                    public function testSomething()
                    {
                        self::assertTrue(true);
                        static::assertTrue(true);
                    }
                };
                PHP,
        ];
    }

    /**
     * @dataProvider provideFix81Cases
     *
     * @requires PHP 8.1
     */
    public function testFix81(string $expected, ?string $input = null): void
    {
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<int, array{string}>
     */
    public static function provideFix81Cases(): iterable
    {
        yield [
            '<?php
                class FooTest extends TestCase
                {
                    public function testFoo()
                    {
                        $a = $this::assertTrue(...);
                    }
                }
            ',
        ];
    }

    /**
     * @requires PHPUnit ^10.0
     */
    public function testPHPUnit10(): void
    {
        self::assertPHPUnit(PhpUnitTargetVersion::VERSION_10_0);
    }

    /**
     * @requires PHPUnit ^11.0
     */
    public function testPHPUnit11(): void
    {
        self::assertPHPUnit(PhpUnitTargetVersion::VERSION_11_0);
    }

    /**
     * @requires PHPUnit ^12.0
     */
    public function testPHPUnit12(): void
    {
        self::assertPHPUnit(PhpUnitTargetVersion::VERSION_NEWEST);
    }

    /**
     * @requires PHPUnit ^13.0
     */
    public function testPHPUnit13(): void
    {
        self::fail('Hello, please implement me, and add new case for PHPUnit 13.');
    }

    /**
     * @param PhpUnitTargetVersion::VERSION_10_0|PhpUnitTargetVersion::VERSION_11_0|PhpUnitTargetVersion::VERSION_NEWEST $version
     */
    private static function assertPHPUnit(string $version): void
    {
        $fixer = new PhpUnitTestCaseStaticMethodCallsFixer();
        $fixer->configure(['target' => $version]);
        $configuredMethods = \Closure::bind(static fn (PhpUnitTestCaseStaticMethodCallsFixer $fixer): array => $fixer->configuration['methods'], null, PhpUnitTestCaseStaticMethodCallsFixer::class)($fixer);

        $methodsForVersion = [];
        foreach ($configuredMethods as $methodName => $calType) {
            self::assertSame(PhpUnitTestCaseStaticMethodCallsFixer::CALL_TYPE_THIS, $calType);
            $methodsForVersion[] = $methodName;
        }
        sort($methodsForVersion);

        $fixerReflection = new \ReflectionClass(PhpUnitTestCaseStaticMethodCallsFixer::class);
        $testCaseReflection = new \ReflectionClass(TestCase::class);

        $expectedMethodsForVersion = [];

        /** @var string $method */
        foreach (array_keys($fixerReflection->getConstant('METHODS')) as $method) {
            if (!$testCaseReflection->hasMethod($method)) {
                continue;
            }
            if ($testCaseReflection->getMethod($method)->isStatic()) {
                continue;
            }
            $expectedMethodsForVersion[] = $method;
        }
        sort($expectedMethodsForVersion);

        self::assertSame($expectedMethodsForVersion, $methodsForVersion);
    }
}
